//
// Game.cpp
//

#include "pch.h"
#include "Game.h"
#include <string>
#include <d3dcompiler.h>
#include <DirectXTK/WICTextureLoader.h>
#include <DirectXTK/ScreenGrab.h>
#include <shellapi.h>
#include <Wincodec.h>

extern void ExitGame();

using namespace DirectX;

struct Vertex
{
    XMFLOAT3 Position;
    XMFLOAT2 UV;

    static constexpr D3D11_INPUT_ELEMENT_DESC InputLayouts[] = {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
        {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0}
    };
};

#if _DEBUG
constexpr UINT kShaderCompileFlags = D3DCOMPILE_SKIP_OPTIMIZATION | D3DCOMPILE_DEBUG;
#else
constexpr UINT kShaderCompileFlags = 0;
#endif

//constexpr std::uint32_t kWidth = 1920;
//constexpr std::uint32_t kHeight = 1080;

wrl::ComPtr<ID3DBlob> CompileVertexShader(ID3D11Device* device, const wchar_t* path)
{
    wrl::ComPtr<ID3DBlob> byteCode;
    wrl::ComPtr<ID3DBlob> errors;
    DX::ThrowIfFailed(D3DCompileFromFile(path, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "main", "vs_5_0", kShaderCompileFlags, 0, byteCode.GetAddressOf(), errors.GetAddressOf()));
    if (errors)
    {
        //error.
        throw std::exception{ static_cast<const char*>(errors->GetBufferPointer()) };
    }
    return byteCode;
}

wrl::ComPtr<ID3DBlob> CompilePixelShader(ID3D11Device* device, const wchar_t* path)
{
    wrl::ComPtr<ID3DBlob> byteCode;
    wrl::ComPtr<ID3DBlob> errors;
    DX::ThrowIfFailed(D3DCompileFromFile(path, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "main", "ps_5_0", kShaderCompileFlags, 0, byteCode.GetAddressOf(), errors.GetAddressOf()));
    if (errors)
    {
        //error.
        auto e = static_cast<const char*>(errors->GetBufferPointer());
        throw std::exception{ e };
    }
    return byteCode;
}

Game::Game()
{
    m_deviceResources = std::make_unique<DX::DeviceResources>();
    m_deviceResources->RegisterDeviceNotify(this);
}

// Initialize the Direct3D resources required to run.
void Game::Initialize(HWND window, int width, int height)
{
    m_deviceResources->SetWindow(window, width, height);
    GetDefaultSize(m_windowWidth, m_windowHeight);
    m_deviceResources->CreateDeviceResources();
    CreateDeviceDependentResources();

    m_deviceResources->CreateWindowSizeDependentResources();
    CreateWindowSizeDependentResources();
    Clear();
    //m_deviceResources->GetD3DDeviceContext()->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    //GaussianBlur(2);
    //SaveWICTextureToFile(m_deviceResources->GetD3DDeviceContext(), m_outputTex.Get(), GUID_ContainerFormatPng, L".\\output.png");

    /*for (int i = 0; i < 1000; ++i)
    {
        GaussianBlur(2);
    }

    for (int i = 0; i < 1000; ++i)
    {
        KawaseBlur();
    }*/

    // TODO: Change the timer settings if you want something other than the default variable timestep mode.
    // e.g. for 60 FPS fixed timestep update logic, call:
    /*
    m_timer.SetFixedTimeStep(true);
    m_timer.SetTargetElapsedSeconds(1.0 / 60);
    */
}

#pragma region Frame Update
// Executes the basic game loop.
void Game::Tick()
{
    m_timer.Tick([&]()
    {
        Update(m_timer);
    });

    Render();
}

// Updates the world.
void Game::Update(DX::StepTimer const& timer)
{
    float elapsedTime = float(timer.GetElapsedSeconds());

    // TODO: Add your game logic here.
    elapsedTime;
}
#pragma endregion

#pragma region Frame Render
// Draws the scene.
void Game::Render()
{
    // Don't try to render anything before the first Update.
    if (m_timer.GetFrameCount() == 0)
    {
        return;
    }

    Clear();

    m_deviceResources->PIXBeginEvent(L"Render");
    auto context = m_deviceResources->GetD3DDeviceContext();

    GaussianBlurOpt(3);
    //KawaseBlur(5);
    //RenderTex(/*m_outputTexView.Get()*/m_tempTexView.Get(), m_deviceResources->GetRenderTargetView(), m_outputPS.Get(), m_deviceResources->GetDepthStencilView());
    RenderTex(m_outputTexView.Get(), m_deviceResources->GetRenderTargetView(), m_outputPS.Get(), m_deviceResources->GetDepthStencilView());

    m_deviceResources->PIXEndEvent();

    // Show the new frame.
    m_deviceResources->Present();
}

// Helper method to clear the back buffers.
void Game::Clear()
{
    m_deviceResources->PIXBeginEvent(L"Clear");

    // Clear the views.
    auto context = m_deviceResources->GetD3DDeviceContext();
    auto renderTarget = m_deviceResources->GetRenderTargetView();
    auto depthStencil = m_deviceResources->GetDepthStencilView();

    context->ClearRenderTargetView(renderTarget, Colors::CornflowerBlue);
    context->ClearDepthStencilView(depthStencil, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    context->OMSetRenderTargets(1, &renderTarget, depthStencil);

    // Set the viewport.
    auto viewport = m_deviceResources->GetScreenViewport();
    context->RSSetViewports(1, &viewport);

    m_deviceResources->PIXEndEvent();
}
#pragma endregion

#pragma region Message Handlers
// Message handlers
void Game::OnActivated()
{
    // TODO: Game is becoming active window.
}

void Game::OnDeactivated()
{
    // TODO: Game is becoming background window.
}

void Game::OnSuspending()
{
    // TODO: Game is being power-suspended (or minimized).
}

void Game::OnResuming()
{
    m_timer.ResetElapsedTime();

    // TODO: Game is being power-resumed (or returning from minimize).
}

void Game::OnWindowMoved()
{
    auto r = m_deviceResources->GetOutputSize();
    m_deviceResources->WindowSizeChanged(r.right, r.bottom);
}

void Game::OnWindowSizeChanged(int width, int height)
{
    m_windowWidth = width;
    m_windowHeight = height;
    if (!m_deviceResources->WindowSizeChanged(width, height))
        return;
    
    CreateWindowSizeDependentResources();

    // TODO: Game window is being resized.
}

// Properties
void Game::GetDefaultSize(std::uint32_t& width, std::uint32_t& height) const
{
    // TODO: Change to desired default window size (note minimum size is 320x200).
    width = 1920;
    height = 1080;
}
#pragma endregion

#pragma region Direct3D Resources
// These are the resources that depend on the device.
void Game::CreateDeviceDependentResources()
{
    const auto device = m_deviceResources->GetD3DDevice();
    ReadInputPicture(device);
    CompileCommonShaders(device);
    CreateCommonVertexIndexBuffer(device);
    CreateSampler(device);
    InitGaussianBlurResources(device);
    CreateCommonResources(device);
    CompileKawaseShaders(device);
}

// Allocate all memory resources that change on a window SizeChanged event.
void Game::CreateWindowSizeDependentResources()
{
    // TODO: Initialize windows-size dependent objects here.
}

void Game::CompileCommonShaders(ID3D11Device* device)
{
    {
        const auto byteCode = CompileVertexShader(device, L".\\CommonVS.hlsl");
        device->CreateInputLayout(Vertex::InputLayouts, std::size(Vertex::InputLayouts), byteCode->GetBufferPointer(),
            byteCode->GetBufferSize(), m_inputLayout.GetAddressOf());
        device->CreateVertexShader(byteCode->GetBufferPointer(), byteCode->GetBufferSize(), nullptr, m_commonVS.GetAddressOf());
    }
    {
        const auto byteCode = CompilePixelShader(device, L".\\OutputPS.hlsl");
        device->CreatePixelShader(byteCode->GetBufferPointer(), byteCode->GetBufferSize(), nullptr, m_outputPS.GetAddressOf());
    }
}

void Game::CreateCommonVertexIndexBuffer(ID3D11Device* device)
{
    {
        //vertex buffer.
        //input NDC Coord directly.
        float width = 1.0f;
        float height = 1.0f;
        //float width = static_cast<float>(m_windowWidth) / m_inputWidth;
        //float height = static_cast<float>(m_windowHeight) / m_inputHeight;
        const Vertex vertices[] = {
            { { -1.0f, 1.0f, 0.0f},{ 0.0f, 0.0f } },
            { { 1.0f, -1.0f, 0.0f},{ width, height } },
            { { -1.0f, -1.0f, 0.0f},{ 0.0f, height } },
            { { 1.0f, 1.0f, 0.0f},{ width, 0.0f }  }
        };
        CD3D11_BUFFER_DESC desc{ sizeof(vertices), D3D11_BIND_VERTEX_BUFFER };
        D3D11_SUBRESOURCE_DATA data = {};
        data.pSysMem = vertices;
        DX::ThrowIfFailed(device->CreateBuffer(&desc, &data, m_vertexBuffer.GetAddressOf()));
    }
    {
        //index buffer
        const std::uint16_t indices[] = {
            0, 1, 2, 0, 3, 1
        };
        CD3D11_BUFFER_DESC desc{ sizeof(indices), D3D11_BIND_INDEX_BUFFER };
        D3D11_SUBRESOURCE_DATA data = {};
        data.pSysMem = indices;
        DX::ThrowIfFailed(device->CreateBuffer(&desc, &data, m_indexBuffer.GetAddressOf()));
    }
}

void Game::ReadInputPicture(ID3D11Device * device)
{
    int argc = 0;
    const auto cmdArgs = CommandLineToArgvW(GetCommandLine(), &argc);
    if (cmdArgs == nullptr || argc < 2) return;
    const auto filePath = cmdArgs[1];
    wrl::ComPtr<ID3D11Resource> resource;
    DX::ThrowIfFailed(CreateWICTextureFromFile(device, filePath, resource.GetAddressOf(), m_orignalTexView.GetAddressOf()));
    DX::ThrowIfFailed(resource.As(&m_orignalTex));
    D3D11_TEXTURE2D_DESC desc;
    m_orignalTex->GetDesc(&desc);
    m_inputWidth = desc.Width;
    m_inputHeight = desc.Height;
}

void Game::CreateSampler(ID3D11Device* device)
{
    CD3D11_SAMPLER_DESC desc{ CD3D11_DEFAULT{} };
    //desc.Filter = D3D11_FILTER_MIN_LINEAR_MAG_MIP_POINT;
    device->CreateSamplerState(&desc, m_sampler.GetAddressOf());
}

void Game::CreateCommonResources(ID3D11Device* device)
{
    //output tex
    CD3D11_TEXTURE2D_DESC desc{ DXGI_FORMAT_R32G32B32A32_FLOAT, m_inputWidth, m_inputHeight };
    desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
    device->CreateTexture2D(&desc, nullptr, m_outputTex.GetAddressOf());
    CD3D11_SHADER_RESOURCE_VIEW_DESC viewDesc{ D3D_SRV_DIMENSION_TEXTURE2D };
    device->CreateShaderResourceView(m_outputTex.Get(), &viewDesc, m_outputTexView.GetAddressOf());

    //rts
    {
        CD3D11_RENDER_TARGET_VIEW_DESC desc{ D3D11_RTV_DIMENSION_TEXTURE2D };
        device->CreateRenderTargetView(m_outputTex.Get(), &desc, m_outputRT.GetAddressOf());

        CD3D11_RENDER_TARGET_VIEW_DESC tempRTDesc{ D3D11_RTV_DIMENSION_TEXTURE2D };
        device->CreateRenderTargetView(m_tempTex.Get(), &tempRTDesc, m_tempRT.GetAddressOf());
    }
}

void Game::RenderTex(ID3D11ShaderResourceView * source, ID3D11RenderTargetView * dest, ID3D11PixelShader * pixelShader, ID3D11DepthStencilView* strencil)
{
    auto context = m_deviceResources->GetD3DDeviceContext();
    context->OMSetRenderTargets(1, &dest, strencil);
    context->ClearRenderTargetView(dest, Colors::White);
    if (strencil) context->ClearDepthStencilView(strencil, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    const auto vb = m_vertexBuffer.Get();
    const UINT strides[] = { sizeof(Vertex) };
    const UINT offset[] = { 0 };
    context->IASetVertexBuffers(0, 1, &vb, strides, offset);
    context->IASetIndexBuffer(m_indexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
    context->VSSetShader(m_commonVS.Get(), nullptr, 0);
    context->IASetInputLayout(m_inputLayout.Get());
    context->PSSetShader(pixelShader, nullptr, 0);
    context->PSSetShaderResources(0, 1, &source);
    context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    ID3D11SamplerState* samplers[] = { m_sampler.Get() };
    context->PSSetSamplers(0, 1, samplers);
    context->DrawIndexed(6, 0, 0);
    ID3D11ShaderResourceView* null[] = { nullptr };
    context->PSSetShaderResources(0, 1, null);
}

void Game::InitGaussianBlurResources(ID3D11Device* device)
{
    {
        const auto byteCode = CompilePixelShader(device, L"BlurVertical.hlsl");
        device->CreatePixelShader(byteCode->GetBufferPointer(), byteCode->GetBufferSize(), nullptr, m_blurVertical.GetAddressOf());
    }
    {
        const auto byteCode = CompilePixelShader(device, L"BlurHorizonal.hlsl");
        device->CreatePixelShader(byteCode->GetBufferPointer(), byteCode->GetBufferSize(), nullptr, m_blurHorizonal.GetAddressOf());
    }
    {
        const auto byteCode = CompilePixelShader(device, L"BlurVerticalOpt.hlsl");
        device->CreatePixelShader(byteCode->GetBufferPointer(), byteCode->GetBufferSize(), nullptr, m_blurVerticalOpt.GetAddressOf());
    }
    {
        const auto byteCode = CompilePixelShader(device, L"BlurHorizonalOpt.hlsl");
        device->CreatePixelShader(byteCode->GetBufferPointer(), byteCode->GetBufferSize(), nullptr, m_blurHorizonalOpt.GetAddressOf());
    }

    {
        D3D11_TEXTURE2D_DESC desc;
        m_orignalTex->GetDesc(&desc);
        desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
        device->CreateTexture2D(&desc, nullptr, m_tempTex.GetAddressOf());
    }
    {
        CD3D11_SHADER_RESOURCE_VIEW_DESC desc{ D3D_SRV_DIMENSION_TEXTURE2D };
        device->CreateShaderResourceView(m_tempTex.Get(), &desc, m_tempTexView.GetAddressOf());
    }
}

void Game::CompileKawaseShaders(ID3D11Device * device)
{
    for (int i = 0; i < std::size(m_blurPasses); ++i)
    {
        auto& shader = m_blurPasses[i];
        const auto shaderName = L"KawasePass" + std::to_wstring(i + 1) + L".hlsl";
        const auto bc = CompilePixelShader(device, shaderName.c_str());
        device->CreatePixelShader(bc->GetBufferPointer(), bc->GetBufferSize(), nullptr, shader.GetAddressOf());
    }
}

void Game::GaussianBlur(std::uint32_t count)
{
    RenderTex(m_orignalTexView.Get(), m_tempRT.Get(), m_blurHorizonal.Get());
    RenderTex(m_tempTexView.Get(), m_outputRT.Get(), m_blurVertical.Get());
    for (int i = 0; i < count - 1; ++i)
    {
        RenderTex(m_outputTexView.Get(), m_tempRT.Get(), m_blurHorizonal.Get());
        RenderTex(m_tempTexView.Get(), m_outputRT.Get(), m_blurVertical.Get());
    }
}

void Game::GaussianBlurOpt(std::uint32_t count)
{
    RenderTex(m_orignalTexView.Get(), m_tempRT.Get(), m_blurHorizonalOpt.Get());
    RenderTex(m_tempTexView.Get(), m_outputRT.Get(), m_blurVerticalOpt.Get());
    for (int i = 0; i < count - 1; ++i)
    {
        RenderTex(m_outputTexView.Get(), m_tempRT.Get(), m_blurHorizonalOpt.Get());
        RenderTex(m_tempTexView.Get(), m_outputRT.Get(), m_blurVerticalOpt.Get());
    }
}


void Game::KawaseBlur(std::uint32_t count)
{
    RenderTex(m_orignalTexView.Get(), m_tempRT.Get(), m_blurPasses[0].Get());
    RenderTex(m_tempTexView.Get(), m_outputRT.Get(), m_blurPasses[1].Get());
    RenderTex(m_outputTexView.Get(), m_tempRT.Get(), m_blurPasses[2].Get());
    RenderTex(m_tempTexView.Get(), m_outputRT.Get(), m_blurPasses[2].Get());
    RenderTex(m_outputTexView.Get(), m_tempRT.Get(), m_blurPasses[3].Get());
}

void Game::OnDeviceLost()
{
    // TODO: Add Direct3D resource cleanup here.
}

void Game::OnDeviceRestored()
{
    CreateDeviceDependentResources();

    CreateWindowSizeDependentResources();
}
#pragma endregion
